﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Runtime;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

using Jack.RedisLib;

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

using Newtonsoft.Json;

using StackExchange.Redis;

namespace Jack.RedisLib
{
    public partial class RedisClient : IRedisClient, IDisposable
    {
        private readonly IOptions<RedisOptions> _options;
        private readonly ILogger<RedisClient> _logger;
        private readonly string _instance;
        private readonly SemaphoreSlim _connectionLock;
        private volatile ConnectionMultiplexer _connection;
        private readonly ConcurrentDictionary<int, IDatabase> _databases = new();
        private readonly JsonSerializerSettings _jsonSerializerSettings;

        public RedisClient(IOptions<RedisOptions> options, ILogger<RedisClient> logger)
        {
            _options = options;
            _logger = logger;
            _instance = _options.Value.InstanceName;
            if (!string.IsNullOrWhiteSpace(_instance) && _instance.EndsWith(":") == false)
            {
                _instance = $"{_instance}:";
            }

            _connectionLock = new SemaphoreSlim(1, 1);

            _jsonSerializerSettings = new JsonSerializerSettings
            {
                //DateFormatHandling = DateFormatHandling.IsoDateFormat,
                //DateParseHandling = DateParseHandling.DateTime,
                DefaultValueHandling = DefaultValueHandling.Ignore,
                //FloatFormatHandling = FloatFormatHandling.String,
                NullValueHandling = NullValueHandling.Ignore,
                DateFormatString = "yyyy'-'MM'-'dd'T'HH':'mm':'ss",
                //MissingMemberHandling = MissingMemberHandling.Ignore,
                //StringEscapeHandling = StringEscapeHandling.Default,
            };
        }

        public RedisValue ConvertObjectValueToRedisValue<T>(T value)
        {
            if (value == null) return RedisValue.Null;
            if (value is string v) return v;

            Type valueType = value.GetType();
            if (valueType.IsValueType) return Convert.ToString(value);

            return JsonConvert.SerializeObject(value, _jsonSerializerSettings);
        }

        public RedisValue[] BatchConvertObjectValueToRedisValue<T>(List<T> values)
        {
            if (values == null || values.Count == 0) return [];

            RedisValue[] redisValues = new RedisValue[values.Count];
            int i = 0;
            foreach (T value in values)
            {
                redisValues[i++] = ConvertObjectValueToRedisValue(value);
            }

            return redisValues;
        }

        public T ConvertRedisValueToObjectValue<T>(RedisValue redisValue)
            where T : class
        {
            if (redisValue == RedisValue.Null) return default;
            if (typeof(T) == typeof(string)) return (string)redisValue as T;

            return JsonConvert.DeserializeObject<T>(redisValue);
        }

        public T? ConvertRedisValueToValueTypeValue<T>(RedisValue redisValue)
            where T : struct
        {
            if (redisValue == RedisValue.Null) return default;
            T value = (T)Convert.ChangeType(Convert.ToString(redisValue), typeof(T));
            return value;
        }

        public RedisKey ConvertStringKeyToRedisKey(string key)
        {
            return GetKeyForInstance(key);
        }

        public RedisKey[] BatchConvertStringKeyToRedisKey(List<string> keys)
        {
            if (keys == null || keys.Count == 0) return [];

            RedisKey[] redisKeys = new RedisKey[keys.Count];
            int i = 0;
            foreach (string key in keys)
            {
                redisKeys[i++] = ConvertStringKeyToRedisKey(key);
            }

            return redisKeys;
        }

        public KeyValuePair<RedisKey, RedisValue>[] BatchConvertKeyValuesToRedisKeyValues<T>(Dictionary<string, T> keyValues)
        {
            if (keyValues == null || keyValues.Count == 0)
            {
                return [];
            }

            KeyValuePair<RedisKey, RedisValue>[] redisKeyValues = new KeyValuePair<RedisKey, RedisValue>[keyValues.Count];

            int i = 0;
            foreach (KeyValuePair<string, T> keyValue in keyValues)
            {
                redisKeyValues[i++] = new KeyValuePair<RedisKey, RedisValue>(ConvertStringKeyToRedisKey(keyValue.Key), ConvertObjectValueToRedisValue(keyValue.Value));
            }

            return redisKeyValues;
        }

        private async Task<IDatabase> GetConnectedDatabase(int db = -1, CancellationToken cancellationToken = default)
        {
            db = (db < 0) ? _options.Value.Database : db;
            if (_databases.TryGetValue(db, out var database) && _connection.IsConnected)
            {
                return database;
            }

            try
            {
                await _connectionLock.WaitAsync(cancellationToken);

                if (_databases.TryGetValue(db, out database) && _connection.IsConnected)
                {
                    return database;
                }

                _connection = await ConnectionMultiplexer.ConnectAsync(_options.Value.ConnectionString, _options.Value.Debug ? Console.Out : null);
                database = _connection.GetDatabase(db, null);
                _databases.AddOrUpdate(db, database, (int key, IDatabase value) => database);

                return database;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "redis连接失败");
                return null;
            }
            finally
            {
                _connectionLock.Release();
            }
        }

        private async Task<ConnectionMultiplexer> GetConnection(CancellationToken cancellationToken = default)
        {
            if (_connection != null && _connection.IsConnected) return _connection;

            try
            {
                await _connectionLock.WaitAsync(cancellationToken);
                if (_connection != null && _connection.IsConnected) return _connection;
                _connection = await ConnectionMultiplexer.ConnectAsync(_options.Value.ConnectionString, _options.Value.Debug ? Console.Out : null);

                return _connection;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "redis连接失败");
                return null;
            }
            finally
            {
                _connectionLock.Release();
            }
        }

        public async Task<IDatabase> GetDatabase(int db = -1, CancellationToken cancellationToken = default) => await GetConnectedDatabase(db, cancellationToken);

        public async Task<bool> KeyDelete(string key, int db = -1, CancellationToken cancellationToken = default)
        {
            IDatabase database = await GetConnectedDatabase(db, cancellationToken);
            if (database == null) return false;

            RedisKey redisKey = ConvertStringKeyToRedisKey(key);
            return await database.KeyDeleteAsync(redisKey);
        }

        public async Task<long> KeyDelete(List<string> keys, int db = -1, CancellationToken cancellationToken = default)
        {
            IDatabase database = await GetConnectedDatabase(db, cancellationToken);
            if (database == null) return -1;

            RedisKey[] redisKeys = BatchConvertStringKeyToRedisKey(keys);
            return await database.KeyDeleteAsync(redisKeys);
        }

        public async Task<bool> KeyExists(string key, int db = -1, CancellationToken cancellationToken = default)
        {
            IDatabase database = await GetConnectedDatabase(db, cancellationToken);
            if (database == null) return false;

            RedisKey redisKey = ConvertStringKeyToRedisKey(key);
            return await database.KeyExistsAsync(redisKey);
        }

        public async Task<long> KeyExists(List<string> keys, int db = -1, CancellationToken cancellationToken = default)
        {
            IDatabase database = await GetConnectedDatabase(db, cancellationToken);
            if (database == null) return -1;

            RedisKey[] redisKeys = BatchConvertStringKeyToRedisKey(keys);
            return await database.KeyExistsAsync(redisKeys);
        }

        public async Task<bool> KeyExpire(string key, TimeSpan? expiry, int db = -1, CancellationToken cancellationToken = default)
        {
            IDatabase database = await GetConnectedDatabase(db, cancellationToken);
            if (database == null) return false;

            RedisKey redisKey = ConvertStringKeyToRedisKey(key);
            return await database.KeyExpireAsync(redisKey, expiry);
        }

        public async Task<bool> KeyExpire(string key, DateTime? expiry, int db = -1, CancellationToken cancellationToken = default)
        {
            IDatabase database = await GetConnectedDatabase(db, cancellationToken);
            if (database == null) return false;

            RedisKey redisKey = ConvertStringKeyToRedisKey(key);
            return await database.KeyExpireAsync(redisKey, expiry);
        }

        public async Task<TimeSpan?> KeyIdleTime(string key, int db = -1, CancellationToken cancellationToken = default)
        {
            IDatabase database = await GetConnectedDatabase(db, cancellationToken);
            if (database == null) return default;

            RedisKey redisKey = ConvertStringKeyToRedisKey(key);
            return await database.KeyIdleTimeAsync(redisKey);
        }

        public async Task<bool> KeyPersist(string key, int db = -1, CancellationToken cancellationToken = default)
        {
            IDatabase database = await GetConnectedDatabase(db, cancellationToken);
            if (database == null) return false;

            RedisKey redisKey = ConvertStringKeyToRedisKey(key);
            return await database.KeyPersistAsync(redisKey);
        }

        public async Task<bool> KeyRename(string key, string newKey, int db = -1, CancellationToken cancellationToken = default)
        {
            IDatabase database = await GetConnectedDatabase(db, cancellationToken);
            if (database == null) return false;

            RedisKey redisKey = ConvertStringKeyToRedisKey(key);
            RedisKey newRedisKey = ConvertStringKeyToRedisKey(key);
            return await database.KeyRenameAsync(redisKey, newRedisKey);
        }

        public async Task<TimeSpan?> KeyTimeToLive(string key, int db = -1, CancellationToken cancellationToken = default)
        {
            IDatabase database = await GetConnectedDatabase(db, cancellationToken);
            if (database == null) return default;

            RedisKey redisKey = ConvertStringKeyToRedisKey(key);
            return await database.KeyTimeToLiveAsync(redisKey);
        }

        public async Task<RedisType> KeyType(string key, int db = -1, CancellationToken cancellationToken = default)
        {
            IDatabase database = await GetConnectedDatabase(db, cancellationToken);
            if (database == null) return RedisType.Unknown;

            RedisKey redisKey = ConvertStringKeyToRedisKey(key);
            return await database.KeyTypeAsync(redisKey);
        }

        public async Task<bool> KeyTouch(string key, int db = -1, CancellationToken cancellationToken = default)
        {
            IDatabase database = await GetConnectedDatabase(db, cancellationToken);
            if (database == null) return false;

            RedisKey redisKey = ConvertStringKeyToRedisKey(key);
            return await database.KeyTouchAsync(redisKey);
        }

        public async Task<long> KeyTouch(List<string> keys, int db = -1, CancellationToken cancellationToken = default)
        {
            IDatabase database = await GetConnectedDatabase(db, cancellationToken);
            if (database == null) return -1;

            RedisKey[] redisKeys = BatchConvertStringKeyToRedisKey(keys);
            return await database.KeyTouchAsync(redisKeys);
        }

        public async Task<T[]> Sort<T>(string key, long skip = 0L, long take = -1L, Order order = Order.Ascending, SortType sortType = SortType.Numeric, string pattern = default, List<string> patterns = default, int db = -1, CancellationToken cancellationToken = default)
            where T : class
        {
            IDatabase database = await GetConnectedDatabase(db, cancellationToken);
            if (database == null) return [];

            RedisKey redisKey = ConvertStringKeyToRedisKey(key);
            RedisValue redisPattern = ConvertObjectValueToRedisValue(pattern);
            RedisValue[] redisPatterns = patterns == default ? null : BatchConvertObjectValueToRedisValue(patterns);

            RedisValue[] redisValues = await database.SortAsync(redisKey, skip, take, order, sortType, redisPattern, redisPatterns);

            T[] values = new T[redisValues.Length];
            for (int i = 0; i < redisValues.Length; i++)
            {
                values[i] = ConvertRedisValueToObjectValue<T>(redisValues[i]);
            }

            return values;
        }

        public async Task<T?[]> SortForValueType<T>(string key, long skip = 0L, long take = -1L, Order order = Order.Ascending, SortType sortType = SortType.Numeric, string pattern = default, List<string> patterns = default, int db = -1, CancellationToken cancellationToken = default)
            where T : struct
        {
            IDatabase database = await GetConnectedDatabase(db, cancellationToken);
            if (database == null) return [];

            RedisKey redisKey = ConvertStringKeyToRedisKey(key);
            RedisValue redisPattern = ConvertObjectValueToRedisValue(pattern);
            RedisValue[] redisPatterns = patterns == default ? null : BatchConvertObjectValueToRedisValue(patterns);

            RedisValue[] redisValues = await database.SortAsync(redisKey, skip, take, order, sortType, redisPattern, redisPatterns);

            T?[] values = new T?[redisValues.Length];
            for (int i = 0; i < redisValues.Length; i++)
            {
                values[i] = ConvertRedisValueToValueTypeValue<T>(redisValues[i]);
            }

            return values;
        }

        public async IAsyncEnumerable<string> ScanKeysForFirstServer(string pattern = default, int pageSize = 250, long cursor = 0L, int pageOffset = 0, int db = -1, [EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            db = (db < 0) ? _options.Value.Database : db;
            ConnectionMultiplexer connection = await GetConnection(cancellationToken);
            if (connection == null) yield break;

            var endPoints = connection.GetEndPoints();
            if (endPoints == null || endPoints.Length == 0) yield break;

            var endPoint = endPoints.First();
            var server = connection.GetServer(endPoint);
            RedisValue redisPattern = ConvertObjectValueToRedisValue(pattern);
            if (cancellationToken.IsCancellationRequested) yield break;

            await foreach (RedisKey redisKey in server.KeysAsync(db, redisPattern, pageSize, cursor, pageOffset))
            {
                yield return redisKey;
                if (cancellationToken.IsCancellationRequested) yield break;
            }
        }

        public async IAsyncEnumerable<string> ScanKeysForAllServer(string pattern = default, int pageSize = 250, long cursor = 0L, int pageOffset = 0, int db = -1, [EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            db = (db < 0) ? _options.Value.Database : db;
            ConnectionMultiplexer connection = await GetConnection(cancellationToken);
            if (connection == null) yield break;

            var endPoints = connection.GetEndPoints();
            if (endPoints == null || endPoints.Length == 0) yield break;

            foreach (var endPoint in endPoints)
            {
                if (cancellationToken.IsCancellationRequested) yield break;

                var server = connection.GetServer(endPoint);
                RedisValue redisPattern = ConvertObjectValueToRedisValue(pattern);
                if (cancellationToken.IsCancellationRequested) yield break;

                await foreach (RedisKey redisKey in server.KeysAsync(db, redisPattern, pageSize, cursor, pageOffset))
                {
                    yield return redisKey;
                    if (cancellationToken.IsCancellationRequested) yield break;
                }

                if (cancellationToken.IsCancellationRequested) yield break;
            }
        }

        public async IAsyncEnumerable<string> ScanKeysForSpecialServer(EndPoint endPoint, string pattern = default, int pageSize = 250, long cursor = 0L, int pageOffset = 0, int db = -1, [EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            db = (db < 0) ? _options.Value.Database : db;
            ConnectionMultiplexer connection = await GetConnection(cancellationToken);
            if (connection == null) yield break;

            var server = connection.GetServer(endPoint);
            RedisValue redisPattern = ConvertObjectValueToRedisValue(pattern);
            if (cancellationToken.IsCancellationRequested) yield break;

            await foreach (RedisKey redisKey in server.KeysAsync(db, redisPattern, pageSize, cursor, pageOffset))
            {
                yield return redisKey;
                if (cancellationToken.IsCancellationRequested) yield break;
            }
        }

        public async Task<IGrouping<string, KeyValuePair<string, string>>[]> Info(string section = null, CancellationToken cancellationToken = default)
        {
            ConnectionMultiplexer connection = await GetConnection(cancellationToken);
            if (connection == null) return default;

            var endPoints = connection.GetEndPoints();
            if (endPoints == null || endPoints.Length == 0) return default;

            var endPoint = endPoints.First();
            var server = connection.GetServer(endPoint);
            RedisValue redisSection = ConvertObjectValueToRedisValue(section);

            return await server.InfoAsync(redisSection);
        }

        public string GetKeyForInstance(string key)
        {
            if (string.IsNullOrWhiteSpace(_instance)) return key;
            if (key.StartsWith(_instance)) return key;

            return $"{_instance}{key}";
        }

        public void Dispose()
        {
            _connectionLock?.Dispose();
            LockTakeSemaphoreSlim?.Dispose();
            if (_connection != null)
            {
                _connection.Close(true);
                _connection.Dispose();
            }
        }
    }
}
